# encoding=utf-8

"""Trivial MySQL ORM."""

import datetime
import logging
import os
import time
import yaml

import MySQLdb
import MySQLdb.constants.FIELD_TYPE as ftype


def _unicode(str):
    return unicode(str, "utf-8")


CONVERTERS = {
    ftype.DECIMAL: float,
    ftype.DOUBLE: float,
    ftype.FLOAT: float,
    ftype.INT24: int,
    ftype.LONG: int,
    ftype.LONGLONG: int,
    ftype.SHORT: int,
    ftype.TINY: int,
    ftype.BIT: int,

    ftype.CHAR: _unicode,
    ftype.STRING: _unicode,
    ftype.VARCHAR: _unicode,
    ftype.VAR_STRING: _unicode,
}


def _date_to_ts(date):
    return time.mktime(date.timetuple())


class DBClient:
    conn = None

    @classmethod
    def fetch(cls, sql, params=None):
        return cls.execute(sql, params).fetchall()

    @classmethod
    def execute(cls, sql, params=None, retry=1):
        """Executes a statement, returns the cursor.  Handles time-out errors and reconnects."""
        try:
            cur = cls._connect().cursor()
            cur.execute(cls._rewrite_sql(sql, params))
            return cur
        except MySQLdb.OperationalError, e:
            if e[0] != 2006 or not retry:
                raise e
            cls._reconnect()
            return cls.execute(sql, params, retry - 1)

    @classmethod
    def commit(cls):
        try:
            cls._connect().commit()
        except MySQLdb.OperationalError, e:
            if e[0] != 2006:
                raise e
            cls._reconnect()

    @classmethod
    def rollback(cls):
        try:
            cls._connect().rollback()
        except MySQLdb.OperationalError, e:
            if e[0] != 2006:
                raise e
            cls._reconnect()

    @classmethod
    def build_where(cls, conditions):
        if not isinstance(conditions, dict):
            raise TypeError("conditions must be a dictionary")

        wheres = []
        params = []

        for k, v in conditions.items():
            if v is None:
                wheres.append("`%s` IS NULL" % k)
            else:
                wheres.append("`%s` = ?" % k)
                params.append(v)

        return " AND ".join(wheres), params

    @classmethod
    def _connect(cls):
        if cls.conn is None:
            config = yaml.load(file("config.yaml", "rb").read())
            args = config["mysql"]
            args["conv"] = CONVERTERS
            cls.conn = MySQLdb.connect(**args)

            cur = cls.conn.cursor()
            cur.execute("SET NAMES utf8")
            del cur
        return cls.conn

    @classmethod
    def _reconnect(cls):
        """Closes the current connection and opens a new one."""
        cls.conn = None
        return cls._connect()

    @classmethod
    def _rewrite_sql(cls, sql, params):
        if not params:
            return sql

        parts = sql.split("?")

        parts_idx = 1
        param_idx = 0

        while param_idx < len(params):
            value = cls._rewrite_param(params[param_idx])
            parts.insert(parts_idx, value)
            parts_idx += 2
            param_idx += 1

        sql = "".join(parts)
        logging.debug("SQL: %s" % sql)
        return sql

    @classmethod
    def _rewrite_param(cls, value):
        if value is None:
            return "NULL"

        if isinstance(value, (str, unicode)):
            return "'" + cls._connect().escape_string(value) + "'"

        return str(value)


class Model(dict):
    table_name = None
    fields = None
    key_name = "id"

    def getattr(self, k):
        return self.get(k)

    def setattr(self, k,v ):
        self[k] = v

    def to_dict(self, base=None):
        """Converts the entity to a dictionary, adds the 'key' element."""
        result = dict(self)
        result["key"] = self.key()
        return result

    def key(self):
        return self.get(self.key_name)

    def put(self):
        try:
            if self.get(self.key_name):
                return self._update()
            return self._insert()
        finally:
            DBClient.commit()

    def delete(self):
        if self.get(self.key_name):
            DBClient.execute("DELETE FROM `%s` WHERE `%s` = ?" % (self.get_table_name(), self.key_name), (self.key(), ))
            self[self.key_name] = None

    def _insert(self):
        fields = [f for f in self.fields if f != self.key_name]

        sql_fields = ", ".join(["`%s`" % f for f in fields])
        sql_qmarks = ", ".join(["?"] * len(fields))

        sql = "INSERT INTO `%s` (%s) VALUES (%s)" % (self.get_table_name(), sql_fields, sql_qmarks)
        params = [self.get(f) for f in fields]

        DBClient.execute(sql, params)

    def _update(self):
        fields = [f for f in self.fields if f != self.key_name]

        parts = ", ".join(["`%s` = ?" % f for f in fields])
        params = [self.get(f) for f in fields]

        sql = "UPDATE `%s` SET %s WHERE `%s` = ?" % (self.get_table_name(), parts, self.key_name)
        params.append(self.key())

        DBClient.execute(sql, params)

    @classmethod
    def find(cls, conditions=None):
        """Finds records that match the specified conditions."""
        sql_fields = ", ".join(["`%s`" % f for f in cls.fields])

        if not conditions:
            sql = "SELECT %s FROM `%s`" % (sql_fields, cls.get_table_name())
            params = None
        else:
            sql_where, params = DBClient.build_where(conditions)
            sql = "SELECT %s FROM `%s` WHERE %s" % (sql_fields, cls.get_table_name(), sql_where)

        return cls.find_by_sql(sql, params)

    @classmethod
    def find_by_sql(cls, sql, params=None):
        sql = cls._rewrite_sql(sql)
        return [cls._from_row(row) for row in DBClient.fetch(sql, params)]

    @classmethod
    def get_by_key(cls, key):
        values = cls.find({
            cls.key_name: key,
        })
        if values:
            return values[0]

    @classmethod
    def _from_row(cls, row):
        items = [(f, row[idx]) for idx, f in enumerate(cls.fields)]
        return cls.from_dict(dict(items))

    @classmethod
    def from_dict(cls, d):
        return cls(d)

    @classmethod
    def get_all(cls):
        return cls.find()

    @classmethod
    def get_table_name(cls):
        if cls.table_name is not None:
            return cls.table_name
        return cls.__name__

    @classmethod
    def _rewrite_sql(cls, sql):
        """Inserts field and table names."""
        if sql.startswith("SELECT * "):
            sql_fields = ", ".join(["`%s`" % f for f in cls.fields])
            sql = "SELECT %s %s" % (sql_fields, sql[9:])
        if "__TABLE__" in sql:
            sql = sql.replace("__TABLE__", "`%s`" % cls.get_table_name())
        return sql

    @classmethod
    def delete_all(cls):
        sql = "DELETE FROM __TABLE__"
        return DBClient.execute(cls._rewrite_sql(sql))


def commit():
    """Commits the current transaction."""
    DBClient.commit()


def rollback():
    """Undoes the transaction."""
    DBClient.rollback()


__all__ = ["Model", "commit", "rollback"]
